前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 事件机制 events

30 秒速记

  • EventEmitter 就是一个发布订阅:on 订阅、emit 发布、off 取消
  • emit 是同步的:按注册顺序一个个调监听器,全部跑完才返回
  • error 事件特殊:没人监听时 emit('error') 会直接抛异常,可能让进程退出
  • 同一事件超过 10 个监听器会打 MaxListenersExceededWarning,多半是监听器泄漏
  • 监听器用普通函数时 this 是 emitter 本身,箭头函数则不是

events 模块的核心是 EventEmitter,说白了就是 Node.js 内置的发布订阅实现。 一边用 on 挂监听器,另一边 emit 触发,http、stream、process 都是在它上面搭的。理解它要抓住几个点:emit 是同步调用,不会扔到下一轮事件循环;监听器按注册顺序执行;error 事件没人接会直接抛出来。线上我见得最多的问题是监听器泄漏,每次请求都往一个全局 emitter 上 on 一次又不 off,控制台出现超过 10 个监听器的警告就该查了。

理解「事件机制 events」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

EventEmitter 的内部可以简化理解成一个「事件名 → 监听器数组」的表:

const EventEmitter = require('events');

const e = new EventEmitter();
e.on('data', (x) => console.log('A', x));
e.on('data', (x) => console.log('B', x));

console.log('before');
const hasListener = e.emit('data', 1); // 同步依次调用 A、B
console.log('after', hasListener);

// before
// A 1
// B 1
// after true
@前端进阶之旅: 代码已经复制到剪贴板

几条规则都能从这个结构推出来:on 往数组末尾追加,prependListener 插到最前面;emit 遍历数组同步调用,返回值表示有没有监听器;once 包了一层,调用前先把自己从数组里删掉;off / removeListener 按函数引用查找,所以传匿名函数是删不掉的。

error 事件要单独记:

const e2 = new EventEmitter();
e2.on('error', (err) => console.error('caught:', err.message));
e2.emit('error', new Error('boom')); // caught: boom
// 如果没有上面那行 on('error'),这里会直接 throw,进程退出
@前端进阶之旅: 代码已经复制到剪贴板

另外 emit 时会先拷贝一份当前的监听器数组,所以在监听器执行过程中新增或移除监听器,不影响这一次 emit,只影响下一次。

面试官追问

追问 1emitter.emit('error', new Error('x')),但没有任何 error 监听器,会怎样?
参考回答

直接把这个错误抛出来,没被 try/catch 包住的话进程就崩了。所以自己写的 EventEmitter 子类、拿到的 stream,都要挂一个 error 监听器。

追问 2控制台出现 MaxListenersExceededWarning: 11 listeners added,怎么处理?
参考回答

先别急着 setMaxListeners(0) 把警告关掉,它多半是在提醒你泄漏。看是哪个事件,一般是每次请求或每次组件挂载都 on 了一次却没 off,把注册挪到初始化阶段,或者在结束时成对移除。

追问 3监听器是 async 函数,里面抛错了会怎样?
参考回答

emit 不会等它,也接不到这个 rejection,最后变成 unhandledRejection,新版本 Node.js 默认会让进程退出。可以 new EventEmitter({ captureRejections: true }),让 rejection 转成 error 事件,或者监听器里自己 try/catch。

追问 4监听器里用箭头函数,this 指向什么?
参考回答

箭头函数没有自己的 this,指向定义时外层的 this,不是 emitter。要用 this 拿 emitter 就写普通 function,不过更推荐直接用闭包里的变量,别依赖 this。

追问 5EventEmitter 和浏览器的 EventTarget 有什么区别?
参考回答

EventTarget 有事件对象、冒泡、preventDefault;EventEmitter 的 emit 能传任意多个参数,没有冒泡。另外 Node.js 里也有 EventTarget,主要给 AbortSignal 这类 Web 标准 API 用,业务代码里还是 EventEmitter 为主。

# 模块概览

30 秒速记

  • 几乎所有核心模块都基于它:stream、http.Server、net.Socket、process、child_process
  • 自定义:class Foo extends EventEmitter {},记得在构造函数里调 super()
  • 常用 API 就几个:on / once / emit / off,再加 listenerCount、prependListener
  • 读文档时看每个类列出的事件,比如 stream 的 data / end / error / close
  • 适合「一对多、不关心谁在听」的通知;需要返回值或等结果的场景用函数调用或 Promise

events 是 Node.js 里最基础的模块之一,http、stream、process 这些常用对象都是 EventEmitter 的实例。 所以学会它,等于学会了一半 Node.js 核心 API 的用法:server.on('request')、stream.on('data')、process.on('exit') 用的都是同一套机制。自己写模块时,继承 EventEmitter 就能对外暴露事件,适合「状态变了通知一下,谁关心谁来听」的场景。但它不适合需要拿返回值的调用,emit 只告诉你有没有人听,拿不到监听器的结果。

events模块是node的核心模块之一,几乎所有常用的node模块都继承了events模块,比如http、fs等。

模块本身非常简单,API虽然也不少,但常用的就那么几个,这里举几个简单例子。

面试官追问

追问 1process.on('exit') 里能做异步操作吗?比如上报日志?
参考回答

不能,exit 事件里只能做同步操作,异步的回调根本没机会执行,进程就退了。要优雅退出,监听 SIGTERM / beforeExit,在那里做完异步清理再主动 process.exit()。

追问 2自己写一个继承 EventEmitter 的类,构造函数里忘了 super(),会怎样?
参考回答

class 语法下,派生类构造函数里不调 super() 就访问 this 会直接抛 ReferenceError。老的 util.inherits 写法则要手动 EventEmitter.call(this),漏了会导致内部的监听器表没初始化。

追问 3模块 A 状态变了要通知 B、C、D,用事件还是直接调函数?
参考回答

B、C、D 是固定的、而且 A 要关心它们的处理结果,就直接调函数,调用关系一眼能看清。如果订阅方不固定,A 不想依赖它们,用事件解耦更合适。事件用多了最大的问题是追踪困难,搜不到谁触发了谁。

追问 4emit 的返回值是什么?
参考回答

布尔值,表示这个事件有没有监听器,true 代表至少调到了一个。它不是监听器的返回值,想拿结果得换成回调或 Promise。

追问 5怎么查一个 emitter 上某个事件挂了多少监听器?
参考回答

emitter.listenerCount('data') 拿数量,emitter.listeners('data') 拿函数数组,emitter.eventNames() 列出所有有监听器的事件名。排查泄漏时把它们定时打出来,看数量是不是一直在涨。

# 基础例子

30 秒速记

  • 六个例子覆盖了日常全部用法:单个监听、多个监听、once、先 emit 后 on、同步执行、移除监听
  • 多个监听器按注册顺序依次执行,想插队用 prependListener
  • once 只触发一次,触发前就会被移除
  • emit 时没人听,这次事件直接丢掉,不会补发
  • 最容易记错的是例子 5:emit 同步执行完所有监听器才往下走

这组例子其实就讲了 EventEmitter 的五条规则,记住它们基本就不会用错。 一是监听器按注册顺序执行;二是 once 注册的只执行一次;三是事件不会缓存,emit 时还没注册的监听器收不到;四是 emit 是同步的,所有监听器跑完才会执行 emit 后面那行代码;五是移除监听器要传同一个函数引用。面试最爱问的是第四条,很多人以为事件天然是异步的,其实异步的是「什么时候

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
      • 模块概览
      • 基础例子
        • 例子1:单个事件监听器
        • 例子2:同个事件,多个事件监听器
        • 例子3:只运行一次的事件监听器
        • 例子4:注册事件监听器前,事件先触发
        • 例子5:异步执行,还是顺序执行
        • 例子6:移除事件监听器
      • 相关链接
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

事件机制 events|Nodejs专题系列

# 事件机制 events

30 秒速记

  • EventEmitter 就是一个发布订阅:on 订阅、emit 发布、off 取消
  • emit 是同步的:按注册顺序一个个调监听器,全部跑完才返回
  • error 事件特殊:没人监听时 emit('error') 会直接抛异常,可能让进程退出
  • 同一事件超过 10 个监听器会打 MaxListenersExceededWarning,多半是监听器泄漏
  • 监听器用普通函数时 this 是 emitter 本身,箭头函数则不是

events 模块的核心是 EventEmitter,说白了就是 Node.js 内置的发布订阅实现。 一边用 on 挂监听器,另一边 emit 触发,http、stream、process 都是在它上面搭的。理解它要抓住几个点:emit 是同步调用,不会扔到下一轮事件循环;监听器按注册顺序执行;error 事件没人接会直接抛出来。线上我见得最多的问题是监听器泄漏,每次请求都往一个全局 emitter 上 on 一次又不 off,控制台出现超过 10 个监听器的警告就该查了。

理解「事件机制 events」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 event-loop 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

EventEmitter 的内部可以简化理解成一个「事件名 → 监听器数组」的表:

const EventEmitter = require('events');

const e = new EventEmitter();
e.on('data', (x) => console.log('A', x));
e.on('data', (x) => console.log('B', x));

console.log('before');
const hasListener = e.emit('data', 1); // 同步依次调用 A、B
console.log('after', hasListener);

// before
// A 1
// B 1
// after true
@前端进阶之旅: 代码已经复制到剪贴板

几条规则都能从这个结构推出来:on 往数组末尾追加,prependListener 插到最前面;emit 遍历数组同步调用,返回值表示有没有监听器;once 包了一层,调用前先把自己从数组里删掉;off / removeListener 按函数引用查找,所以传匿名函数是删不掉的。

error 事件要单独记:

const e2 = new EventEmitter();
e2.on('error', (err) => console.error('caught:', err.message));
e2.emit('error', new Error('boom')); // caught: boom
// 如果没有上面那行 on('error'),这里会直接 throw,进程退出
@前端进阶之旅: 代码已经复制到剪贴板

另外 emit 时会先拷贝一份当前的监听器数组,所以在监听器执行过程中新增或移除监听器,不影响这一次 emit,只影响下一次。

面试官追问

追问 1emitter.emit('error', new Error('x')),但没有任何 error 监听器,会怎样?
参考回答

直接把这个错误抛出来,没被 try/catch 包住的话进程就崩了。所以自己写的 EventEmitter 子类、拿到的 stream,都要挂一个 error 监听器。

追问 2控制台出现 MaxListenersExceededWarning: 11 listeners added,怎么处理?
参考回答

先别急着 setMaxListeners(0) 把警告关掉,它多半是在提醒你泄漏。看是哪个事件,一般是每次请求或每次组件挂载都 on 了一次却没 off,把注册挪到初始化阶段,或者在结束时成对移除。

追问 3监听器是 async 函数,里面抛错了会怎样?
参考回答

emit 不会等它,也接不到这个 rejection,最后变成 unhandledRejection,新版本 Node.js 默认会让进程退出。可以 new EventEmitter({ captureRejections: true }),让 rejection 转成 error 事件,或者监听器里自己 try/catch。

追问 4监听器里用箭头函数,this 指向什么?
参考回答

箭头函数没有自己的 this,指向定义时外层的 this,不是 emitter。要用 this 拿 emitter 就写普通 function,不过更推荐直接用闭包里的变量,别依赖 this。

追问 5EventEmitter 和浏览器的 EventTarget 有什么区别?
参考回答

EventTarget 有事件对象、冒泡、preventDefault;EventEmitter 的 emit 能传任意多个参数,没有冒泡。另外 Node.js 里也有 EventTarget,主要给 AbortSignal 这类 Web 标准 API 用,业务代码里还是 EventEmitter 为主。

# 模块概览

30 秒速记

  • 几乎所有核心模块都基于它:stream、http.Server、net.Socket、process、child_process
  • 自定义:class Foo extends EventEmitter {},记得在构造函数里调 super()
  • 常用 API 就几个:on / once / emit / off,再加 listenerCount、prependListener
  • 读文档时看每个类列出的事件,比如 stream 的 data / end / error / close
  • 适合「一对多、不关心谁在听」的通知;需要返回值或等结果的场景用函数调用或 Promise

events 是 Node.js 里最基础的模块之一,http、stream、process 这些常用对象都是 EventEmitter 的实例。 所以学会它,等于学会了一半 Node.js 核心 API 的用法:server.on('request')、stream.on('data')、process.on('exit') 用的都是同一套机制。自己写模块时,继承 EventEmitter 就能对外暴露事件,适合「状态变了通知一下,谁关心谁来听」的场景。但它不适合需要拿返回值的调用,emit 只告诉你有没有人听,拿不到监听器的结果。

events模块是node的核心模块之一,几乎所有常用的node模块都继承了events模块,比如http、fs等。

模块本身非常简单,API虽然也不少,但常用的就那么几个,这里举几个简单例子。

面试官追问

追问 1process.on('exit') 里能做异步操作吗?比如上报日志?
参考回答

不能,exit 事件里只能做同步操作,异步的回调根本没机会执行,进程就退了。要优雅退出,监听 SIGTERM / beforeExit,在那里做完异步清理再主动 process.exit()。

追问 2自己写一个继承 EventEmitter 的类,构造函数里忘了 super(),会怎样?
参考回答

class 语法下,派生类构造函数里不调 super() 就访问 this 会直接抛 ReferenceError。老的 util.inherits 写法则要手动 EventEmitter.call(this),漏了会导致内部的监听器表没初始化。

追问 3模块 A 状态变了要通知 B、C、D,用事件还是直接调函数?
参考回答

B、C、D 是固定的、而且 A 要关心它们的处理结果,就直接调函数,调用关系一眼能看清。如果订阅方不固定,A 不想依赖它们,用事件解耦更合适。事件用多了最大的问题是追踪困难,搜不到谁触发了谁。

追问 4emit 的返回值是什么?
参考回答

布尔值,表示这个事件有没有监听器,true 代表至少调到了一个。它不是监听器的返回值,想拿结果得换成回调或 Promise。

追问 5怎么查一个 emitter 上某个事件挂了多少监听器?
参考回答

emitter.listenerCount('data') 拿数量,emitter.listeners('data') 拿函数数组,emitter.eventNames() 列出所有有监听器的事件名。排查泄漏时把它们定时打出来,看数量是不是一直在涨。

# 基础例子

30 秒速记

  • 六个例子覆盖了日常全部用法:单个监听、多个监听、once、先 emit 后 on、同步执行、移除监听
  • 多个监听器按注册顺序依次执行,想插队用 prependListener
  • once 只触发一次,触发前就会被移除
  • emit 时没人听,这次事件直接丢掉,不会补发
  • 最容易记错的是例子 5:emit 同步执行完所有监听器才往下走

这组例子其实就讲了 EventEmitter 的五条规则,记住它们基本就不会用错。 一是监听器按注册顺序执行;二是 once 注册的只执行一次;三是事件不会缓存,emit 时还没注册的监听器收不到;四是 emit 是同步的,所有监听器跑完才会执行 emit 后面那行代码;五是移除监听器要传同一个函数引用。面试最爱问的是第四条,很多人以为事件天然是异步的,其实异步的是「什么时候 emit」,不是 emit 本身。

下面一共是6个例子,都非常简单,可以直接拷贝出来运行。例子5比较有意思,虽然也并不复杂,但确实是容易记错的点,感兴趣的同学可以看下。

理解「基础例子」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

面试官追问

追问 1同一个事件挂了两个监听器,第一个抛了异常,第二个还会执行吗?
参考回答

不会。emit 就是同步循环调用,第一个抛错会直接中断循环,异常抛给调用 emit 的那段代码,后面的监听器都不执行。

追问 2想让某个监听器排在已有的监听器前面执行,怎么写?
参考回答

用 emitter.prependListener(name, fn),只执行一次的版本是 prependOnceListener。注意依赖执行顺序本身就是个坏味道,能不依赖尽量不依赖。

追问 3同一个函数 on 了两次,emit 一次会调几次?
参考回答

两次。EventEmitter 不去重,同一个函数可以注册多次。removeListener 一次也只移除一个,要清干净得调多次,或者用 removeAllListeners(name)。

追问 4为什么说事件不会缓存?有什么实际影响?
参考回答

emit 时它只遍历当前已有的监听器,没有就什么都不做,不会存起来等后面的人。所以像 stream 的 data 这种,要在数据开始流动前挂好监听器,不然开头的数据就错过了。

追问 5这些例子能怎么改成 Promise 风格?
参考回答

const { once } = require('events'); const [v] = await once(emitter, 'ready'); 会等到下一次 ready 事件,参数以数组形式返回。如果先触发了 error,这个 Promise 会 reject。

# 例子1:单个事件监听器

← 8.2 二进制解码 string_decoder9.2 实用工具模块 util →

fe